CloudFormation StackSets のデプロイ失敗をSlack通知する
こんにちは。たかやまです。
今回は、こちらの記事のSlack通知版を作成したいと思います。
メール通知をしたい場合には上記記事を参考にしてください。
とりあえずやってみたい方へ
CFnテンプレート
Chatbot版
前提条件 :
Slackワークスペースは設定済みで、SlackワークスペースIDとチャンネルIDを取得済みであること
パラメータ:
SlackWorkspaceId
: SlackワークスペースIDSlackChannelId
: SlackチャンネルID
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Slack Configuration
Parameters:
- SlackWorkspaceId
- SlackChannelId
ParameterLabels:
SlackWorkspaceId:
default: Slack Workspace ID
SlackChannelId:
default: Slack Channel ID
Parameters:
SlackWorkspaceId:
Type: String
Description: Slack Workspace ID
SlackChannelId:
Type: String
Description: Slack Channel ID
Resources:
ChatbotErrorTopicF26444E6:
Type: AWS::SNS::Topic
Properties:
TopicName: ChatbotErrorNotifications
ChatbotErrorTopicPolicy0A68C558:
Type: AWS::SNS::TopicPolicy
Properties:
PolicyDocument:
Statement:
- Action: sns:Publish
Effect: Allow
Principal:
Service: events.amazonaws.com
Resource:
Ref: ChatbotErrorTopicF26444E6
Sid: "0"
Version: "2012-10-17"
Topics:
- Ref: ChatbotErrorTopicF26444E6
ChatbotErrorSlackChannelConfigurationRole0D1A29D8:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: chatbot.amazonaws.com
Version: "2012-10-17"
ChatbotErrorSlackChannel461DCF21:
Type: AWS::Chatbot::SlackChannelConfiguration
Properties:
ConfigurationName: chatbot-error-notifications
IamRoleArn:
Fn::GetAtt:
- ChatbotErrorSlackChannelConfigurationRole0D1A29D8
- Arn
LoggingLevel: ERROR
SlackChannelId:
Ref: SlackChannelId
SlackWorkspaceId:
Ref: SlackWorkspaceId
SnsTopicArns:
- Ref: ChatbotErrorTopicF26444E6
ChatbotErrorRuleF0295058:
Type: AWS::Events::Rule
Properties:
EventPattern:
source:
- aws.cloudformation
detail-type:
- CloudFormation StackSet StackInstance Status Change
detail:
status-details:
detailed-status:
- INOPERABLE
- CANCELLED
- FAILED
- FAILED_IMPORT
- SKIPPED_SUSPENDED_ACCOUNT
State: ENABLED
Targets:
- Arn:
Ref: ChatbotErrorTopicF26444E6
Id: Target0
InputTransformer:
InputPathsMap:
region: $.region
account: $.account
detail-stack-set-arn: $.detail.stack-set-arn
detail-status-details-status: $.detail.status-details.status
detail-status-details-detailed-status: $.detail.status-details.detailed-status
detail-status-details-status-reason: $.detail.status-details.status-reason
InputTemplate: '{"version":"1.0","source":"custom","content":{"textType":"client-markdown","title":":warning: CloudFormation StackSet StackInstance Status Change | <region> | Account: <account>","description":"アカウントID: <account>\nリージョン: <region>\n\nスタックセットARN: <detail-stack-set-arn>\n\nステータス: <detail-status-details-status>\nステータス詳細: <detail-status-details-detailed-status>\nステータス理由: <detail-status-details-status-reason>"}}':
Slack Webhook版
前提条件 :
Slack Webhook URLを取得済みであること
パラメータ:
SlackWebhookUrl
: Slack Webhook URL
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Slack Configuration
Parameters:
- SlackWebhookUrl
ParameterLabels:
SlackWebhookUrl:
default: Slack Webhook URL
Parameters:
SlackWebhookUrl:
Type: String
Description: Slack Webhook URL
Resources:
SlackNotifierLambdaServiceRoleBF51634B:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Version: "2012-10-17"
ManagedPolicyArns:
- Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
SlackNotifierLambdaServiceRoleDefaultPolicy9623029E:
Type: AWS::IAM::Policy
Properties:
PolicyDocument:
Statement:
- Action: organizations:DescribeAccount
Effect: Allow
Resource: "*"
Version: "2012-10-17"
PolicyName: SlackNotifierLambdaServiceRoleDefaultPolicy9623029E
Roles:
- Ref: SlackNotifierLambdaServiceRoleBF51634B
SlackNotifierLambda0EBEA281:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |-
import boto3
import os
import json
import urllib.request
from logging import getLogger, INFO
logger = getLogger()
logger.setLevel(INFO)
organizations = boto3.client("organizations")
SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']
def get_account_name(account_id):
resp = organizations.describe_account(AccountId=account_id)
return resp['Account']['Name']
def lambda_handler(event, context):
# メッセージ作成に必要なパラメータの取得
detail_type = event['detail-type']
stack_set_arn = event['detail'].get('stack-set-arn')
stack_set_name = stack_set_arn.split('/')[1].split(':')[0]
stack_id = event['detail'].get('stack-id')
region = stack_id.split(':')[3]
account_id = stack_id.split(':')[4]
account_name = get_account_name(account_id)
status = event['detail']['status-details'].get('status')
detailed_status = event['detail']['status-details'].get('detailed-status')
status_reason = event['detail']['status-details'].get('status-reason')
# メッセージ作成
title = f":warning: *{detail_type} | {region} | Account: {account_id}*"
message = (f"{title}\n"
f"アカウント: {account_name} ({account_id})\n"
f"リージョン: {region}\n\n"
f"スタックセット名: {stack_set_name}\n"
f"スタックセットARN: {stack_set_arn}\n\n"
f"ステータス: {status}\n"
f"ステータス詳細: {detailed_status}\n"
f"ステータス理由: {status_reason}")
# Slack通知
slack_message = {
'text': message
}
data = json.dumps(slack_message).encode('utf-8')
req = urllib.request.Request(SLACK_WEBHOOK_URL, data=data, headers={'Content-Type': 'application/json'})
try:
response = urllib.request.urlopen(req)
response_body = response.read()
logger.info(f'Slack notification sent successfully: {response_body}')
except urllib.error.HTTPError as e:
logger.error(f'Failed to send Slack notification: {e.reason}')
raise
return {
'statusCode': 200,
'body': json.dumps('Slack notification sent successfully')
} # この行の括弧を追加しました
Environment:
Variables:
SLACK_WEBHOOK_URL:
Ref: SlackWebhookUrl
Handler: index.lambda_handler
Role:
Fn::GetAtt:
- SlackNotifierLambdaServiceRoleBF51634B
- Arn
Runtime: python3.12
DependsOn:
- SlackNotifierLambdaServiceRoleDefaultPolicy9623029E
- SlackNotifierLambdaServiceRoleBF51634B
StackSetsErrorRuleFBA6F6EB:
Type: AWS::Events::Rule
Properties:
EventPattern:
source:
- aws.cloudformation
detail-type:
- CloudFormation StackSet StackInstance Status Change
detail:
status-details:
detailed-status:
- INOPERABLE
- CANCELLED
- FAILED
- FAILED_IMPORT
- SKIPPED_SUSPENDED_ACCOUNT
State: ENABLED
Targets:
- Arn:
Fn::GetAtt:
- SlackNotifierLambda0EBEA281
- Arn
Id: Target0
StackSetsErrorRuleAllowEventRuleWebhookNotifyStackSlackNotifierLambdaD780FA364FFD0DEE:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName:
Fn::GetAtt:
- SlackNotifierLambda0EBEA281
- Arn
Principal: events.amazonaws.com
SourceArn:
Fn::GetAtt:
- StackSetsErrorRuleFBA6F6EB
- Arn
CDKテンプレート
CDKのコードも用意しているので、CDKを利用される方はこちらからご利用ください。
Chatbot版(シンプル版)
こちらはAWS Chatbotを利用してSlackに通知を送る方法です。
以下のようにCANCELLED
とFAILED
のステータスエラーをStackSetsで発生させ通知させてみたいと思います。
冒頭のChatbot版テンプレートをデプロイしてSlackに通知された内容がこちらです。
Chatbotのデフォルト通知はエラー内容がわからないため、こちらの通知はEventBridgeの入力トランスフォーマーを利用して通知内容をカスタマイズしています。
Chatbotデフォルト通知
ただ、入力トランスフォーマーもイベント元の内容を元に整形しているため、アカウント名などの元情報に存在しない情報は付与することはできません。
通知内容にAWSアカウント名を付与したいなど内容をよりカスタマイズしたい場合には、次にご紹介するLambda関数を利用してSlack Webhookを利用して通知します。
Slack Webhook版(AWSアカウント名を付与したい)
メール通知版と同様にアカウント名を付与する場合は、Lambda関数を利用してSlack Webhookを利用して通知します。
Lambda関数内のコードは以下のようになります。
import boto3
import os
import json
import urllib.request
from logging import getLogger, INFO
logger = getLogger()
logger.setLevel(INFO)
organizations = boto3.client("organizations")
SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']
def get_account_name(account_id):
resp = organizations.describe_account(AccountId=account_id)
return resp['Account']['Name']
def lambda_handler(event, context):
# #####
# メッセージ作成に必要なパラメータの取得
# #####
detail_type = event['detail-type']
# ### スタックセット名
stack_set_arn = event['detail'].get('stack-set-arn')
stack_set_name = stack_set_arn.split('/')[1].split(':')[0]
# ### リージョン, アカウントID, アカウント名
stack_id = event['detail'].get('stack-id')
region = stack_id.split(':')[3]
account_id = stack_id.split(':')[4]
account_name = get_account_name(account_id)
# ### ステータス, ステータス詳細, ステータス理由
status = event['detail']['status-details'].get('status')
detailed_status = event['detail']['status-details'].get('detailed-status')
status_reason = event['detail']['status-details'].get('status-reason')
# #####
# メッセージ作成
# #####
title = f":warning: *{detail_type} | {region} | Account: {account_id}*"
message = ( f'{title}\n'
f'アカウント: {account_name} ({account_id})\n'
f'リージョン: {region}\n\n'
f'スタックセット名: {stack_set_name}\n'
f'スタックセットARN: {stack_set_arn}\n\n'
f'ステータス: {status}\n'
f'ステータス詳細: {detailed_status}\n'
f'ステータス理由: {status_reason}')
# #####
# Slack通知
# #####
slack_message = {
'text': message
}
data = json.dumps(slack_message).encode('utf-8')
req = urllib.request.Request(SLACK_WEBHOOK_URL, data=data, headers={'Content-Type': 'application/json'})
try:
response = urllib.request.urlopen(req)
response_body = response.read()
logger.info(f'Slack notification sent successfully: {response_body}')
except urllib.error.HTTPError as e:
logger.error(f'Failed to send Slack notification: {e.reason}')
raise
return {
'statusCode': 200,
'body': json.dumps('Slack notification sent successfully')
}
Webhookを利用した場合の通知は以下のようになります。
アカウント名が付与され、アカウントIDのみの通知よりもわかりやすくなりました。
最後に
CloudFormation StackSetsのデプロイ失敗をSlack通知する場合の方法をご紹介しました。
こちらの内容がどなたかのお役に立てれば幸いです。
以上、たかやま(@nyan_kotaroo)でした。